package edu.buaa.energy.server;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Floats;
import edu.buaa.common.RuntimeEnv;
import edu.buaa.common.server.DBSocketServer;
import edu.buaa.common.transaction.AbstractTransaction;
import edu.buaa.common.transaction.AbstractTransaction.Result;
import edu.buaa.common.transaction.CreateTGraphAggrMaxIndexTx;
import edu.buaa.common.transaction.CreateTGraphTemporalValueIndexTx;
import edu.buaa.energy.EnergyDataTxGenerator;
import edu.buaa.energy.transaction.*;
import edu.buaa.utils.Helper;
import edu.buaa.utils.Pair;
import edu.buaa.utils.Triple;
import org.act.temporalProperty.index.IndexType;
import org.act.temporalProperty.index.value.IndexMetaData;
import org.act.temporalProperty.query.TimePointL;
import org.act.temporalProperty.query.aggr.AggregationIndexQueryResult;
import org.act.temporalProperty.query.aggr.AggregationQuery;
import org.act.temporalProperty.query.aggr.ValueGroupingMap;
import org.act.temporalProperty.util.Slice;
import org.neo4j.graphdb.*;
import org.neo4j.temporal.*;
import org.neo4j.tooling.GlobalGraphOperations;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static org.act.temporalProperty.index.IndexType.AGGR_DURATION;
import static org.act.temporalProperty.index.IndexType.AGGR_MAX;

public class TGraphKernelServer0 extends DBSocketServer.ReqExecutor {

    public static void main(String[] args){
        DBSocketServer server = new DBSocketServer( dbDir(), new TGraphKernelServer0(), 8438);
        RuntimeEnv env = RuntimeEnv.getCurrentEnv();
        String serverCodeVersion = env.name() + "." + Helper.codeGitVersion();
        System.out.println("server code version: "+ serverCodeVersion);
        try {
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static File dbDir(){
        String path = Helper.mustEnv("DB_PATH");
        Preconditions.checkNotNull(path, "need arg: DB_PATH");
        File dbDir = new File(path);
        if( !dbDir.exists()){
            if(dbDir.mkdirs()) return dbDir;
            else throw new IllegalArgumentException("invalid dbDir");
        }else if( !dbDir.isDirectory()){
            throw new IllegalArgumentException("invalid dbDir");
        }
        return dbDir;
    }

    public enum EdgeType implements RelationshipType {
        EDGE_TO
    }
    private static final String TP = "tp";
    private final Map<String, Integer> propMap = initPropMap();

    private Map<String, Integer> initPropMap() {
        HashMap<String, Integer> pMap = new HashMap<>();
        for(int i=0; i< EnergyDataTxGenerator.properties.length; i++){
            pMap.put(EnergyDataTxGenerator.properties[i], i);
        }
        return pMap;
    }

    private final Map<Integer, Long> idMap2Long = new HashMap<>();

    private long id(int rawId){
        Long r = idMap2Long.get(rawId);
        if(r==null){
            throw new NullPointerException("got raw id "+rawId+" which not exist in db.");
        }
        return r;
    }

    @Override
    public void setDB(GraphDatabaseService db){
        this.db = db;
        try(Transaction tx = db.beginTx()){
            for(Node node: GlobalGraphOperations.at(db).getAllNodes()){
                Integer rawId = (Integer) node.getProperty("id");
                Preconditions.checkNotNull(rawId,"should not happen: id is null");
                idMap2Long.put(rawId, node.getId());
//                System.out.println(rawId+" , "+ node.getId());
            }
            tx.success();
        }
    }

    @Override
    public Result execute(String line) throws RuntimeException {
        AbstractTransaction tx = JSON.parseObject(line, AbstractTransaction.class);
        try {
            switch (tx.getTxType()) {
                case tx_import_static_data:
                    return execute((ImportStaticDataTx) tx);
                case tx_import_temporal_data:
                    return execute((ImportTemporalDataTx) tx);
                case tx_update_temporal_data:
                    return execute((UpdateTemporalDataTx) tx);
                case tx_query_snapshot:
                    return execute((SnapshotQueryTx) tx);
                case tx_query_entity_history:
//                    System.out.println(line);
                    return execute((EntityHistoryTx) tx);
                case tx_query_road_by_temporal_condition:
                    EntityTemporalConditionTx etx = (EntityTemporalConditionTx) tx;
                    return execute(etx);
                case tx_query_snapshot_aggr_max:
                    return execute((SnapshotAggrMaxTx) tx);
                case tx_query_snapshot_aggr_duration:
                    return execute((SnapshotAggrDurationTx) tx);

                default:
                    throw new UnsupportedOperationException();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private Result execute(ImportStaticDataTx tx){
        try(Transaction t = db.beginTx()) {
            for (ImportStaticDataTx.EnergyNode node : tx.getNodes()) {
                Node n = db.createNode();
                n.setProperty("id", node.getRawId());
                n.setProperty("name", node.getName());
                n.setProperty("country", node.getCountry());
                n.setProperty("voltage", node.getVoltage());
                n.setProperty("latitude", node.getLatitude());
                n.setProperty("longitude", node.getLongitude());
                idMap2Long.put(node.getRawId(), n.getId());
            }
            for (ImportStaticDataTx.EnergyRel sr : tx.getRels()) {
                Node start = db.getNodeById(id(sr.getFromRawId()));
                Node end = db.getNodeById(id(sr.getToRawId()));
                Relationship r = start.createRelationshipTo(end, EdgeType.EDGE_TO);
                r.setProperty("length", sr.getLength());
                r.setProperty("x", sr.getX());
                r.setProperty("y", sr.getY());
                r.setProperty("limit", sr.getLimit());
            }
            t.success();
        }
        return new Result();
    }

    private Result execute(ImportTemporalDataTx tx) {
        try(Transaction t = db.beginTx()) {
            for(ImportTemporalDataTx.EnergyStatus s : tx.data){
                Node n = db.getNodeById(id(s.getNodeId()));
                TimePoint time = Helper.time(s.getTime());
                setProperties(s.getStatus(), n, time, null);
            }
            t.success();
        }
        return new Result();
    }

    private void setProperties(List<Float> values, Node node, TimePoint begin, TimePoint end){
        ByteBuffer raw = ByteBuffer.allocate(values.size() * 4);
        raw.asFloatBuffer().put(Floats.toArray(values));
        String val = new String(raw.array());
        if(end==null){
            node.setTemporalProperty(TP, begin, val);
        }else{
            node.setTemporalProperty(TP, begin, end, val);
        }
    }

    private float extract(String vArr, String propertyName) {
        FloatBuffer arr = ByteBuffer.wrap(vArr.getBytes()).asFloatBuffer();
        return arr.get(propMap.get(propertyName));
    }

    private Result execute(UpdateTemporalDataTx tx) {
        try(Transaction t = db.beginTx()) {
            Node n = db.getNodeById(id(tx.getEntityId()));
            setProperties(tx.getValues(), n, Helper.time(tx.getStartTime()), Helper.time(tx.getEndTime()));
            t.success();
        }
        return new Result();
    }

    private Result execute(SnapshotQueryTx tx){
        try(Transaction t = db.beginTx()) {
            List<Pair<Integer, Float>> answers = new ArrayList<>();
            for(Node n: GlobalGraphOperations.at(db).getAllNodes()){
                Integer id = (Integer) n.getProperty("id");
                Object vArr = n.getTemporalProperty(TP, Helper.time(tx.getTimestamp()));
                if(vArr instanceof String){
                    float v = extract((String) vArr, tx.getPropertyName());
                    answers.add(Pair.of(id, v));
                }
            }
            SnapshotQueryTx.Result result = new SnapshotQueryTx.Result();
            result.setStatus(answers);
            return result;
        }
    }

//    private float extract(String vArr, String propertyName) {
//        String[] arr = vArr.split(",");
//        return Float.parseFloat(arr[propMap.get(propertyName)]);
//    }


    private Result execute(SnapshotAggrMaxTx tx){
        try(Transaction t = db.beginTx()){
            List<Pair<Integer, Float>> answers = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()){
                Integer id = (Integer) n.getProperty("id");
                TemporalRangeQuery.MaxFloat calc = new TemporalRangeQuery.MaxFloat();
                n.getTemporalProperty(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), new TemporalRangeQuery(){
                    @Override
                    public boolean onNewEntry(long entityId, int propertyId, TimePointL time, Object val) {
                        if(val instanceof String){
                            float vv = extract((String) val, tx.getP());
                            calc.onNewEntry(entityId, propertyId, time, vv);
                        }
                        return false;
                    }
                });
                answers.add(Pair.of(id, (Float) calc.onReturn())); //放入answers中
            }
            SnapshotAggrMaxTx.Result result = new SnapshotAggrMaxTx.Result();
            result.setPropMaxValue(answers);
            return result;
        }
    }

    private Result execute(SnapshotAggrDurationTx tx) {
        TimePoint begin = Helper.time(tx.getT0()), end = Helper.time(tx.getT1());
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Float, Integer>> answers = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()) {
                Integer entity = (Integer) n.getProperty("id");
                TimeIntervalRangeQuery.FloatDuration calc = new TimeIntervalRangeQuery.FloatDuration(begin, end, tx.getIntStartTreeSet());
                Object v = n.getTemporalProperty(tx.getP(), begin, end, new TemporalRangeQuery() {
                    @Override
                    public boolean onNewEntry(long entityId, int propertyId, TimePointL time, Object val) {
                        if(val instanceof String){
                            float vv = extract((String) val, tx.getP());
                            return calc.onNewEntry(entityId, propertyId, time, vv);
                        }
                        return true;
                    }
                });
                if(v instanceof HashMap){
                    HashMap<Float, Integer> m = (HashMap<Float, Integer>) v;
                    m.forEach((k, val) -> answers.add(Triple.of(entity, k, val)));
                }
            }
            SnapshotAggrDurationTx.Result result = new SnapshotAggrDurationTx.Result();
            result.setStatusDuration(answers);
            return result;
        }
    }

    private Result execute(EntityHistoryTx tx) {
        TimePoint begin = Helper.time(tx.getBeginTime()), end = Helper.time(tx.getEndTime());
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Integer, Float>> answers = new ArrayList<>();
            Node n = db.getNodeById(id(tx.getEntity()));
            n.getTemporalProperty(tx.getProp(), begin, end, new TimeIntervalRangeQuery(begin, end){
                @Override
                public void onEntry(TimePointL beginTime, TimePointL endTime, Object val) {
                    if(val instanceof String){
                        float vv = extract((String) val, tx.getProp());
                        answers.add(Triple.of(beginTime.valInt(), endTime.valInt(), vv));
                    }
                }
                @Override public Object returnValue() { return null; }
            });
            EntityHistoryTx.Result result = new EntityHistoryTx.Result();
            result.setHistory(answers);
            return result;
        }
    }

    private Result execute(EntityTemporalConditionTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Integer> res = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()) {
                int entity = (Integer) n.getProperty("id");
                n.getTemporalProperty(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), new TemporalRangeQuery() {
                    @Override
                    public boolean onNewEntry(long entityId, int propertyId, TimePointL time, Object val) {
                        if(val instanceof String) {
                            float v = extract((String) val, tx.getP());
                            if (tx.getVMin() <= v && v <= tx.getVMax()) {
                                res.add(entity);
                                return false;
                            }
                        }
                        return true;
                    }
                    @Override
                    public Object onReturn() {
                        return res;
                    }
                });
            }
            EntityTemporalConditionTx.Result result = new EntityTemporalConditionTx.Result();
            result.setEntities(res);
            return result;
        }
    }

}
